import os
import sqlite3
import time
from twisted.internet import protocol, reactor, task
from twisted.words.protocols import irc

import googdict
import redditscan

class DataStore(object):
  def __init__(self, path):
    initialized = os.path.exists(path)
    self.conn = sqlite3.connect(path)
    self.conn.isolation_level = None
    if not initialized:
      self.initialize()

  def initialize(self):
    self.sql('CREATE TABLE config ('
             'name TEXT,'
             'value TEXT,'
             'PRIMARY KEY (name))')
    self.sql('INSERT INTO config VALUES ("schema", "0")')
    self.sql('INSERT INTO config VALUES ("subreddit", "programming")')
    self.sql('CREATE TABLE channels ('
             'name TEXT NOT NULL, '
             'invited BOOLEAN DEFAULT FALSE, '
             'PRIMARY KEY (name))')
    self.sql('CREATE TABLE items ('
             'name TEXT NOT NULL, '
             'url TEXT NOT NULL, '
             'title TEXT NOT NULL, '
             'author TEXT NOT NULL, '
             'created DATETIME NOT NULL, '
             'posted DATETIME, '
             'over_18 BOOLEAN DEFAULT FALSE, '
             'PRIMARY KEY (name))')

  def sql(self, query, *args, **kwargs):
    cursor = self.conn.cursor()
    cursor.execute(query, args or kwargs)
    return cursor

  def getConfig(self, name, default=None):
    cursor = self.sql('SELECT value FROM config WHERE name = ?', name)
    for row in cursor:
      return row[0]
    else:
      if default is None:
        raise KeyError(name)
      return default

  def setChannelInvited(self, channel, invited):
    # Quick update if channel exists.
    for row in self.sql('SELECT name FROM channels WHERE name = ?', channel):
      self.sql('UPDATE channels SET invited = ? WHERE name = ?',
               invited, channel)
      return
    else:
      # Create new entry for channel.
      self.sql('INSERT INTO channels VALUES (?, ?)', channel, invited)

  def getInvitedChannels(self):
    cursor = self.sql('SELECT name FROM channels WHERE invited')
    for row in cursor:
      yield str(row[0])

  def saveListing(self, listing):
    for item in listing:
      self.saveItem(item)

  def saveItem(self, item):
    # Return if item already exists in DB
    for row in self.sql('SELECT name FROM items WHERE name = ?', item.name):
      return

    self.sql('INSERT INTO items (name, url, title, author, created, over_18) '
             'VALUES (?, ?, ?, ?, ?, ?)',
             item.name, item.url, item.title, item.author, item.created,
             item.over_18)

  def getItemsToPost(self):
    cursor = self.sql('SELECT name, url, title, author, over_18, created'
                      ' FROM items WHERE posted IS NULL')
    for name, url, title, author, over_18, created in cursor:
      yield redditscan.Item(name=name,
                            url=url,
                            title=title,
                            author=author,
                            over_18=over_18,
                            created=created,
                            )

  def markItemsAsPosted(self, names, post_time=None):
    if not post_time:
      post_time = time.time()
    for name in names:
      self.sql('UPDATE items SET posted = ? WHERE name = ?', post_time, name)
  

class RedditBot(irc.IRCClient):
  @property
  def nickname(self):
    return self.factory.nickname

  @property
  def data_store(self):
    return self.factory.data_store

  def announceNewItems(self):
    items = list(self.data_store.getItemsToPost())
    if not items:
      return
    # Try not to be too spammy. If there are two or fewer items, simply announce
    # both. If there are three or more, announce the first one, followed by a
    # summary of the remainder.
    if len(items) < 3:
      msgs = map(str, items)
    else:
      msgs = [str(items[0]), self._summarizeItems(items[1:])]
    channels = list(self.data_store.getInvitedChannels())
    for channel in channels:
      for msg in msgs:
        self.msg(channel, msg)
    self.data_store.markItemsAsPosted(item.name for item in items)

  @staticmethod
  def _summarizeItems(items):
    authors = {}
    for item in items:
      authors[str(item.author)] = authors.get(item.author, 0) + 1
    clauses = ['%d link%s from %s' % (value, 's'[value == 1:], key)
               for key, value in
               sorted(authors.iteritems(), key=lambda x: x[1], reverse=True)]
    if len(clauses) == 1:
      return 'plus %s' % clauses[0]
    else:
      return 'plus %s, and %s' % (', '.join(clauses[:-1]), clauses[-1])

  def serveDefinitions(self, term, channel):
    definitions = googdict.define(term)
    if not definitions:
      self.msg(channel, 'No definitions found for `%s\'', term)
    else:
      for definition in definitions:
        self.msg(channel, definition)

  @staticmethod
  def getNick(user):
    if '!' in user:
      return user[:user.index('!')]
    return user

  def msg(self, target, msg, *args, **kwargs):
    target = self.getNick(target)
    if args:
      msg = msg % args
    elif kwargs:
      msg = msg % kwargs
    print '-> %s: %s' % (target, msg)
    irc.IRCClient.msg(self, target, str(msg))

  def signedOn(self):
    self.factory.connected = True
    self.factory.resetDelay()
    for channel in self.data_store.getInvitedChannels():
      self.join(channel)

  def privmsg(self, user, channel, message):
    print 'privmsg: %s, %s: %s' % (user, channel, message)
    if channel == self.nickname:
      reply_to = user
    else:
      reply_to = channel
    if message.startswith('!def '):
      term = message[len('!def '):].strip()
      if term == 'pederast':
        self.msg(reply_to, 'Shut the fuck up, %s.', self.getNick(user))
        return
      self.serveDefinitions(message[len('!def '):], reply_to)

  def noticed(self, user, channel, message):
    print 'notice: %s, %s: %s' % (user, channel, message)

  def kickedFrom(self, channel, kicker, message):
    self.data_store.setChannelInvited(channel, False)

  def irc_INVITE(self, prefix, params):
    channel = params[-1]
    print 'invited to %s' % channel
    self.data_store.setChannelInvited(channel, True)
    self.join(channel)


class RedditBotFactory(protocol.ReconnectingClientFactory):
  protocol = RedditBot

  def __init__(self, data_store_path, nickname='RedditBot'):
    self.client = None
    self.connected = False
    self.data_store = DataStore(data_store_path)
    self.nickname = nickname
    self.scanning_task = task.LoopingCall(self.scanForNewItems)
    self.scanning_task.start(60)

  def clientConnectionLost(self, connector, reason):
    print 'Lost connection (%s)' % reason
    self.connected = False
    protocol.ReconnectingClientFactory.clientConnectionLost(
        self, connector, reason)

  def clientConnectionFailed(self, connector, reason):
    print 'Connection failed (%s)' % reason
    self.connected = False
    protocol.ReconnectingClientFactory.clientConnectionLost(
        self, connector, reason)

  def buildProtocol(self, addr):
    print 'Building client'
    self.client = protocol.ClientFactory.buildProtocol(self, addr)
    return self.client

  def getServerAddress(self):
    addr = self.data_store.getConfig('server', default='irc.endernet.org')
    if ':' in addr:
      host, port_str = addr.split(':')
      return host, int(port_str)
    else:
      return addr, 6667

  def scanForNewItems(self):
    print 'scan for new items'
    subreddit = self.data_store.getConfig('subreddit')
    print 'call scanner in thread'
    reactor.callInThread(self._scanForNewItemsInThread, subreddit)

  def _scanForNewItemsInThread(self, subreddit):
    print 'fetching listing'
    listing = redditscan.Listing.get(subreddit)
    print 'call processor from thread'
    reactor.callFromThread(self._processListing, listing)

  def _processListing(self, listing):
    print 'saving listing'
    self.data_store.saveListing(listing)
    if self.connected:
      self.client.announceNewItems()


if __name__ == '__main__':
  factory = RedditBotFactory('redditbot.db')
  host, port = factory.getServerAddress()
  reactor.connectTCP(host, port, factory)
  reactor.run()
